package com.zqweb.gulimall.seckill.service.impl;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.zqweb.common.to.SeckillOrderTo;
import com.zqweb.common.utils.R;
import com.zqweb.gulimall.seckill.feign.CouponFeignService;
import com.zqweb.gulimall.seckill.feign.ProductFeignService;
import com.zqweb.gulimall.seckill.interceptor.SeckillLoginInterceptor;
import com.zqweb.gulimall.seckill.sentinel.SentinelFallback;
import com.zqweb.gulimall.seckill.service.SeckillService;
import com.zqweb.gulimall.seckill.to.SeckillSkuRedisTo;
import com.zqweb.gulimall.seckill.vo.SeckillSessionVo;
import com.zqweb.gulimall.seckill.vo.SkuInfoVo;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    private CouponFeignService couponFeignService;
    @Autowired
    private ProductFeignService productFeignService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private RedissonClient redissonClient;
    private final String SECSKILL_SESSIONS_CACHE_PREFIX = "seckill:sessions:";
    private final String SECSKILL_SKUS_CACHE_PREFIX = "seckill:skus";
    private final String SECSKILL_STOCK_SEMAPHORE = "seckill:stock:";

    @Override
    // 上架最近3天要参与秒杀的商品
    public void uploadLate3DaySeckillSkus() {
        // 先获取最近3天要参与秒杀的session活动信息及所关联的所有秒杀商品信息
        R r = couponFeignService.getLate3DaySession();
        if(r != null && r.getCode() == 0){
            List<SeckillSessionVo> seckillSessionVos = r.getData(new TypeReference<List<SeckillSessionVo>>() {
            });
            if(seckillSessionVos != null && seckillSessionVos.size() > 0){
                // 将这些信息保存Redis中
                this.saveForSeckillSessionInfo(seckillSessionVos);

            }
        }
    }

    // 当方法getCurrentSeckillSkusInfo调用出错block了，触发该blockHandler进行处理
    public List<SeckillSkuRedisTo> getCurrentSeckillSkusInfo_BlockHandler(BlockException be){
        // 资源访问出现问题，被限流或被降级后的处理操作
        System.out.println("BlockHandler方式：查询获取秒杀商品失败，原因为：" + be.getMessage());
        return null;
    }


    @Override
//    @SentinelResource(value = "getSeckillSkus",blockHandler = "getCurrentSeckillSkusInfo_BlockHandler",fallback = "getCurrentSeckillSkusInfo_Fallback",fallbackClass = SentinelFallback.class)
    // 查询获取当前时间对应的秒杀活动及所关联的所有秒杀商品信息（从Redis中取即可）
    public List<SeckillSkuRedisTo> getCurrentSeckillSkusInfo() {
        try(Entry entry = SphU.entry("getCurrentSeckillSkusInfo")) {
            // 受保护（熔断降级保护）的业务逻辑
            List<SeckillSkuRedisTo> seckillSkuRedisTos = null;
            Long curTime = new Date().getTime();
            // 从Redis中获取包含seckill:sessions:的key集合（通配符*）
            Set<String> keys = stringRedisTemplate.keys(SECSKILL_SESSIONS_CACHE_PREFIX + "*");
            if(keys != null && keys.size() > 0){
                for (String key : keys) {
                    // 获取到每个key对应秒杀活动的开始时间和结束时间
                    String timeStr = key.replace(SECSKILL_SESSIONS_CACHE_PREFIX, "");
                    Long startTime = Long.parseLong(timeStr.split("_")[0]);
                    Long endTime = Long.parseLong(timeStr.split("_")[1]);
                    if(startTime <= curTime && curTime <= endTime){
                        List<String> sessionsWithSkuIds = stringRedisTemplate.opsForList().range(key, -9999, 9999);
                        if(sessionsWithSkuIds != null && sessionsWithSkuIds.size() > 0){
                            // 参数1：存储的Map对应key的类型  参数2：Map中元素key的类型  参数3：Map中元素value的类型
                            BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECSKILL_SKUS_CACHE_PREFIX);
                            List<String> skuInfoJSONStrs = hashOps.multiGet(sessionsWithSkuIds);
                            if(skuInfoJSONStrs != null && skuInfoJSONStrs.size() > 0){
                                seckillSkuRedisTos = skuInfoJSONStrs.stream().map(skuInfoStr -> {
                                    SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(skuInfoStr, SeckillSkuRedisTo.class);
                                    // 关于是否返回秒杀商品对应的随机码，看是秒杀活动预告还是已经开始了秒杀（这里已经开始了秒杀）
                                    return seckillSkuRedisTo;
                                }).collect(Collectors.toList());
                            }
                        }
                        // 只要根据当前时间匹配到了秒杀活动信息，该秒杀活动是否上架了秒杀商品信息，都要break（可提示还没有商品上架~）
                        break;
                    }

                }
            }
            return seckillSkuRedisTos;
        } catch (BlockException e) {
            // 资源访问出现问题，被限流或被降级后的处理操作
            System.out.println("try-catch方式：查询获取秒杀商品失败，原因为：" + e.getMessage());
            return null;
        }
    }

    @Override
    // 获取指定skuId所关联的秒杀商品（包含优惠信息）
    public SeckillSkuRedisTo getSeckillSkusInfoBySkuId(Long skuId) {
        SeckillSkuRedisTo seckillSkuRedisTo = null;
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECSKILL_SKUS_CACHE_PREFIX);
        String regix = "\\d_" + skuId;// 将秒杀活动id用正则的d表示
        for (String key : hashOps.keys()) {
            boolean matches = Pattern.matches(regix, key);
            if(matches){
                String redisToJSONStr = hashOps.get(key);
                seckillSkuRedisTo = JSON.parseObject(redisToJSONStr, SeckillSkuRedisTo.class);
                Long curTime = new Date().getTime();
                Long startTime = seckillSkuRedisTo.getStartTime();
                Long endTime = seckillSkuRedisTo.getEndTime();
                if(startTime <= curTime && curTime <= endTime){
                    // 若当前时间开始了秒杀活动，默认返回当前skuId商品对应的随机码
                }else{
                    // 若当前时间还未开始秒杀活动，则不返回当前skuId商品对应的随机码
                    seckillSkuRedisTo.setRandomCode(null);
                }
                // 不用返回商品的基本信息，商品详情业务自己获取了
                seckillSkuRedisTo.setSkuInfo(null);
                break;// 返回1个秒杀活动的优惠信息即可
            }
        }
        return seckillSkuRedisTo;
    }

    @Override
    // 秒杀商品（立即抢购）
    // TODO 上架秒杀商品信息的每一格数据，都设有过期时间
    public String secKillGoods(String killId, Integer killNum, String killCode) {
        /*
         先作合法性检查（Redis中是否有对应的存储记录、秒杀时间是否匹配、秒杀商品对应的随机码是否匹配、
         秒杀商品的数量是否正确、登陆用户是否已参与过秒杀：幂等性处理、信号量变化是否成功
         检查通过后，再来快速下单以及MQ的消息通知
          */
        // 1、合法性检查
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECSKILL_SKUS_CACHE_PREFIX);
        String skuRedisToJsonStr = hashOps.get(killId);
        // Redis中是否有对应的存储记录
        if(StringUtils.isEmpty(skuRedisToJsonStr)){
            return null;
        }else{
            SeckillSkuRedisTo skuRedisTo = JSON.parseObject(skuRedisToJsonStr, SeckillSkuRedisTo.class);
            Long curTime = new Date().getTime();
            Long startTime = skuRedisTo.getStartTime();
            Long endTime = skuRedisTo.getEndTime();
            Long ttl = (endTime - startTime);// ms为单位
            // 秒杀时间是否匹配
            if(curTime >= startTime && curTime <= endTime){
                String randomCode = skuRedisTo.getRandomCode();
                // 秒杀商品对应的随机码是否匹配
                if(randomCode.equals(killCode)){
                    // 秒杀商品的数量是否正确
                    if(killNum <= skuRedisTo.getSeckillLimit().intValue()){
                        Long userId = SeckillLoginInterceptor.threadLocal.get().getId();
                        String redisKey = userId + "_" + killId;// userId_sessionId_skuId
                        // 设置该登陆用户的秒杀key是会在对应秒杀活动结束后自动过期的（保留起来没用）
                        Boolean isNoSet = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, killNum.toString(),ttl,TimeUnit.MILLISECONDS);
                        // 登陆用户是否已参与过秒杀：幂等性处理
                        if(isNoSet){
                            // 信号量变化是否成功
                            RSemaphore semaphore = redissonClient.getSemaphore(SECSKILL_STOCK_SEMAPHORE + randomCode);
                            boolean isSetOK = semaphore.tryAcquire(killNum);
                            if(isSetOK){
                                // 2、快速下单以及MQ的消息通知
                                String orderSn = IdWorker.getTimeId();
                                SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                                seckillOrderTo.setOrderSn(orderSn);
                                seckillOrderTo.setPromotionSessionId(skuRedisTo.getPromotionSessionId());
                                seckillOrderTo.setSkuId(skuRedisTo.getSkuId());
                                seckillOrderTo.setSeckillPrice(skuRedisTo.getSeckillPrice());
                                seckillOrderTo.setNum(killNum);
                                seckillOrderTo.setMemberId(userId);

                                rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",seckillOrderTo);

                                return orderSn;
                            }else {
                                return null;
                            }

                        }else{
                            return null;// 之前该登陆用户已经参与过killId对应的秒杀
                        }
                    }else{
                        return null;
                    }
                }else{
                    return null;
                }
            }else{
                return null;
            }
        }
    }

    // 将获取到的最近3天要参与秒杀的session活动信息及所关联的所有秒杀商品信息保存到Redis中
    private void saveForSeckillSessionInfo(List<SeckillSessionVo> seckillSessionVos){
        seckillSessionVos.forEach(secSession -> {
            Long startTime = secSession.getStartTime().getTime();
            Long endTime = secSession.getEndTime().getTime();
            String key = this.SECSKILL_SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            // 如果指定startTime、endTime段内已经上架过秒杀活动及商品信息，就无需再上架了
            if(!stringRedisTemplate.hasKey(key)){
                List<String> skuIds = secSession.getRelations().stream().map(secRelation -> secRelation.getPromotionSessionId() + "_" + secRelation.getSkuId().toString()).collect(Collectors.toList());
                stringRedisTemplate.opsForList().leftPushAll(key,skuIds);// 保存的是秒杀活动所关联的所有要参与秒杀的商品id
            }
        });
        this.saveForSeckillSkusInfo(seckillSessionVos);
    }
    private void saveForSeckillSkusInfo(List<SeckillSessionVo> seckillSessionVos){
        seckillSessionVos.forEach(secSession -> {
            // 构建Redis的Hash操作工具类
            BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(this.SECSKILL_SKUS_CACHE_PREFIX);
            secSession.getRelations().forEach(secRelation -> {
                // 如果指定秒杀活动id及所关联的特定秒杀商品已经参与过上架，就无需重复上架了！
                if(!hashOps.hasKey(secRelation.getPromotionSessionId() + "_" + secRelation.getSkuId().toString())){
                    // 定义存入到Redis中的秒杀商品信息TO
                    SeckillSkuRedisTo skuRedisTo = new SeckillSkuRedisTo();
                    // 1、构建sku的基本数据信息
                    R r = productFeignService.info(secRelation.getSkuId());
                    if(r != null && r.getCode() == 0){
                        SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        if(skuInfo != null){
                            skuRedisTo.setSkuInfo(skuInfo);
                        }
                    }
                    // 2、构建sku的秒杀信息
                    BeanUtils.copyProperties(secRelation,skuRedisTo);
                    // 3、构建sku参与秒杀的开始时间和结束时间
                    skuRedisTo.setStartTime(secSession.getStartTime().getTime());
                    skuRedisTo.setEndTime(secSession.getEndTime().getTime());
                    // 4、设置每一类参与秒杀的商品的随机码，并根据这个随机码在Redis中创建1个信号量，值为该类商品参与秒杀的数量
                    String randomCode = UUID.randomUUID().toString().replace("-", "");
                    skuRedisTo.setRandomCode(randomCode);

// 将商品参与秒杀的数量作为以该类商品生成的随机码构建的分布式信号量的值，可进行限流（若信号量不存在，则根本无法去参与秒杀后续的业务）
                    RSemaphore rSemaphore = redissonClient.getSemaphore(SECSKILL_STOCK_SEMAPHORE + randomCode);
                    rSemaphore.trySetPermits(secRelation.getSeckillCount().intValue());

                    // 以JSON串的形式保存到Redis中
                    String skuJSONStr = JSON.toJSONString(skuRedisTo);
                    hashOps.put(secRelation.getPromotionSessionId() + "_" + secRelation.getSkuId().toString(),skuJSONStr);

                }
            });
        });
    }



}
